"""The main Application and Service classes"""

import os, errno
import gc
import cProfile
from OpenSSL import SSL
import socket
import traceback
import inspect
from greenlet import greenlet

import eof.util as U
import eof.ztk  as ztk

from diesel.hub import EventHub
from diesel import log, Connection, UDPSocket, Loop
from diesel.security import ssl_async_handshake
from diesel import runtime
from diesel.events import WaitPool

try:
	from . import ifaces
except ImportError:
	ifaces = None

try:
	import eof.failure as failure
except ImportError:
	failure = None


YES_PROFILE = ['1', 'on', 'true', 'yes']


class ApplicationEnd(Exception):
	pass


class Application:
	"""The Application represents diesel's main loop--
	the coordinating entity that runs all Services, Loops,
	Client protocol work, etc."""

	def __init__(self, allow_app_replacement=False):
		assert (allow_app_replacement or runtime.current_app is None), "Only one Application instance per program allowed"

		runtime.current_app = self

		if not log.initialized:
			log.quickSetup()

		self.hub   = EventHub()
		self.waits = WaitPool()

		self._run = False
		self._services = []
		self._loops = []

		self.running = set()

	def global_bail(self, msg):
		def bail():
			log.critical("ABORTING: {0}", msg)
			self.halt()
		return bail

	def run(self):
		"""Start up an Application--blocks until the program ends or .halt() is called."""

		profile = os.environ.get('DIESEL_PROFILE', '').lower() in YES_PROFILE
		track_gc = os.environ.get('TRACK_GC', '').lower() in YES_PROFILE
		track_gc_leaks = os.environ.get('TRACK_GC_LEAKS', '').lower() in YES_PROFILE

		if track_gc:
			gc.set_debug(gc.DEBUG_STATS)

		if track_gc_leaks:
			gc.set_debug(gc.DEBUG_LEAK)

		self._run = True
		log.warning('Starting diesel <{0}>', self.hub.describe)

		for service in self._services:
			with U.safecontext('Failure during start {}'.format(service)) as ok:
				service.start()
				service.register(self)

			if not ok:
				self.halt(ok.failure)

		for l in self._loops:
			self.hub.schedule(l.wake)

		self.setup()

		def _main():
			while self._run:
				try:
					self.hub.handle_events()
				except SystemExit:
					log.warning("-- SystemExit raised.. exiting main loop --")
					raise
				except KeyboardInterrupt:
					log.warning("-- KeyboardInterrupt raised.. exiting main loop --")
					break
				except ApplicationEnd as exc:
					if exc.args and isinstance(exc.args[0], BaseException):
						etype  = type(exc.args[0])
						evalue = exc.args[0]
						log.error('-- ApplicationEnd raised (with reason [{etype.__name__}] {evalue})... exiting main loop --'\
							.format(etype=etype, evalue=evalue))
					else:
						log.warning("-- ApplicationEnd raised... exiting main loop --")
					break
				except Exception:
					log.trace().error("-- Unhandled Exception rose to main loop --")

			for service in self._services:
				U.safecall((service, 'stop'))  # @xxx: TCPService & co.

			# @todo: [bw] (?) wait thread, see L{diesel.hub:AbstractEventHub.run_in_thread}

			log.info('Ending diesel application')
			runtime.current_app = None

		def _profiled_main():
			log.warning("(Profiling with cProfile)")

			# NOTE: Scoping Issue:
			# Have to rebind _main to _real_main so it shows up in locals().
			_real_main = _main
			config = {'sort':1}
			statsfile = os.environ.get('DIESEL_PSTATS', None)
			if statsfile:
				config['filename'] = statsfile
			try:
				cProfile.runctx('_real_main()', globals(), locals(), **config)
			except TypeError as e:
				if "sort" in e.args[0]:
					del config['sort']
					cProfile.runctx('_real_main()', globals(), locals(), **config)
				else: raise e

		self.runhub = greenlet(_main if not profile else _profiled_main)
		self.runhub.switch()

	def add_service(self, service):
		"""Add a Service instance to this Application.

	The service will bind to the appropriate port and start
	handling connections when the Application is run()."""

		assert isinstance(service, TCPServer), service

		service.application = self

		if self._run:
			# TODO -- this path doesn't clean up binds yet
			# [bw] ?
			with U.safecontext('Failure during start {}'.format(service)) as ok:
				service.start()
				service.register(self)

			if not ok:
				self.halt(ok.failure)
		else:
			self._services.append(service)

		return service

	def add_loop(self, loop, front=False, keep_alive=False, track=False):
		"""Add a Loop instance to this Application.

	The loop will be started when the Application is run()."""

		if track:
			loop.enable_tracking()

		if keep_alive:
			loop.keep_alive = True

		if self._run:
			self.hub.schedule(loop.wake)
		else:
			if front:
				self._loops.insert(0, loop)
			else:
				self._loops.append(loop)

	def halt(self, reason=None):
		"""Stop this application from running--the initial run() call will return."""

		for service in self._services:
			U.safecall((service, 'stop'))  # @xxx: TCPService & co.

		del self._services[:]

		def raise_end():
			raise ApplicationEnd(reason)

		if reason is not None:
			if failure is not None and isinstance(reason, failure.Failure):
				reason = reason.value
			elif not isinstance(reason, BaseException) and inspect.isclass(reason) and issubclass(reason, BaseException):
				reason = reason()
			else:
				reason = RuntimeError(reason)

		self.hub.schedule(raise_end)  # [bw] для обхода `diesel.console`

	def setup(self):
		"""Do some initialization right before the main loop is entered.

	Called by run()."""


class TCPServer:
	"""A TCP service listening on a certain port, with a protocol
	implemented by a passed connection handler."""

	sock = None
	application = None

	def __init__(self, connection_handler, port, iface='', ssl_ctx=None, track=False, backlog=50):
		"""Given a protocol-implementing callable `connection_handler`, handle connections on port `port`.

	Interface defaults to all interfaces, but overridable with `iface`."""

		self.port = port
		self.iface = iface
		self.connection_handler = connection_handler
		self.ssl_ctx = ssl_ctx
		self.track = track
		self.backlog = backlog

		# Call this last so the connection_handler has a fully-instantiated
		# Service instance at its disposal.
		if hasattr(connection_handler, 'on_service_init') and callable(connection_handler.on_service_init):
			connection_handler.on_service_init(self)

	def register(self, app):
		app.hub.register(self.sock, self.accept_new_connection, None,
			app.global_bail('low-level socket error on bound service'))

	def start(self):
		sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
		sock.setblocking(False)

		try:
			sock.bind((self.iface, self.port))
		except socket.error as exc:
			log.critical('service at {0}:{1} cannot bind: {2}', self.iface or '*', self.port, str(exc))
			raise

		sock.listen(self.backlog)
		self.sock = sock
		self.port = sock.getsockname()[1] # in case of 0 binds

		if ifaces is not None:
			ztk.notify(ifaces.ServiceStartedEvent(self))

	def stop(self):
		if ifaces is not None:
			ztk.notify(ifaces.ServiceStoppingEvent(self))
		if self.sock is not None:
			self.sock.close()

	@property
	def listening(self):
		return self.sock is not None

	def accept_new_connection(self):
		try:
			sock, addr = self.sock.accept()
		except socket.error as exc:
			if exc.errno in (errno.EAGAIN, errno.EINTR):
				return
			raise

		sock.setblocking(False)

		def make_connection():
			c = Connection(sock, addr)
			l = Loop(self.connection_handler, addr)
			l.connection_stack.append(c)
			runtime.current_app.add_loop(l, track=self.track)

		if self.ssl_ctx:
			sock = SSL.Connection(self.ssl_ctx, sock)
			sock.set_accept_state()
			sock.setblocking(False)
			ssl_async_handshake(sock, self.application.hub, make_connection)
		else:
			make_connection()


class Thunk:
	def __init__(self, c):
		self.c = c

	def eval(self):
		return self.c()


class UDPServer(TCPServer):
	"""A UDP server listening on a certain port, with a protocol
	implemented by a passed connection handler."""

	def __init__(self, connection_handler, port, iface=''):
		super().__init__(connection_handler, port, iface)
		self.remote_addr = (None, None)

	def register(self, app):
		pass

	def start(self):
		sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
		# unsure if the following two lines are necessary for UDP
		sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
		sock.setblocking(False)

		try:
			sock.bind((self.iface, self.port))
		except socket.error as exc:
			log.critical('service at {0}:{1} cannot bind: {2}', self.iface or '*', self.port, str(exc))
			raise

		self.sock = sock

		conn = UDPSocket(self, sock)
		loop = Loop(self.connection_handler)
		loop.connection_stack.append(conn)

		runtime.current_app.add_loop(loop)


def quickstart(*args, **kw):
    if '__app' in kw:
        app = kw.pop('__app')
    else:
        app = Application(**kw)
    args = list(args)
    for a in args:
        if isinstance(a, Thunk):
            a = a.eval()
        if isinstance(a, (list, tuple)):
            args.extend(a)
        elif isinstance(a, (TCPServer, UDPServer)):
            app.add_service(a)
        elif isinstance(a, Loop):
            app.add_loop(a)
        elif callable(a):
            app.add_loop(Loop(a))
    app.run()

def quickstop():
    runtime.current_app.halt()
